#!/usr/bin/env python
# viewcfg - A script for viewing the CFG of SIL and LLVM IR -*- python -*-
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ----------------------------------------------------------------------------
#
# For vim users: use the following lines in .vimrc...
#
#   com! -nargs=? Funccfg silent ?{$?,/^}/w !viewcfg <args>
#   com! -range -nargs=? Viewcfg silent <line1>,<line2>w !viewcfg <args>
#
# ...to add these commands:
#
#   :Funccfg        displays the CFG of the current SIL/LLVM function.
#   :<range>Viewcfg displays the sub-CFG of the selected range.
#
# Note: viewcfg should be in the $PATH and .dot files should be associated
# with the Graphviz app.
#
# ----------------------------------------------------------------------------

from __future__ import print_function

import re
import subprocess
import sys
import tempfile


def help():
    print("""\
Usage:

viewcfg [output-suffix] < file

By default all CFGs are opened in the same window.
Use the a unique output-suffix to open a CFG in a new window.
""")


class Block(object):

    current_index = 0

    def __init__(self, name, preds):
        self.name = name
        self.content = None
        self.preds = []
        self.succs = None
        self.last_line = None
        self.index = Block.current_index
        Block.current_index += 1
        if preds is not None:
            for pred in re.split("[, %]", preds):
                can_pred = pred.strip()
                if can_pred:
                    self.preds.append(can_pred)

    def add_line(self, text):
        if self.content is None:
            self.content = ""
        escaped_text = re.sub(r'([\\<>{}"|])', r'\\\1', text[0:80]).rstrip()
        self.content += escaped_text + '\\l'
        self.last_line = text

    def get_succs(self):
        if self.succs is None:
            self.succs = []
            if self.last_line is not None:
                for match in re.finditer(r'\bbb[0-9]+\b', self.last_line):
                    self.succs.append(match.group())

                it = re.finditer(r'\blabel %"?([^\s"]+)\b', self.last_line)
                for match in it:
                    self.succs.append(match.group(1))

        return self.succs


def main():
    suffix = ""
    if len(sys.argv) >= 2:
        if sys.argv[1].startswith('-'):
            help()
            return
        suffix = sys.argv[1]

    block_list = []
    block_map = {}
    cur_block = None
    sil_block_pattern = re.compile(r'^(\S+)(\(.*\))?: *(\/\/ *Preds:(.*))?$')
    llvm_block_pattern1 = re.compile(r'^"?([^\s"]+)"?: *; *preds =(.*)?$')
    llvm_block_pattern2 = re.compile(r'^(\d+):? *; *preds =(.*)?$')

    # Scan the input file.

    for line in sys.stdin:
        sil_block_match = sil_block_pattern.match(line)
        llvm_block_match1 = llvm_block_pattern1.match(line)
        llvm_block_match2 = llvm_block_pattern2.match(line)
        block_name = None
        preds = None
        if sil_block_match:
            block_name = sil_block_match.group(1)
            preds = sil_block_match.group(4)
        elif llvm_block_match1:
            block_name = llvm_block_match1.group(1)
            preds = llvm_block_match1.group(2)
        elif llvm_block_match2:
            block_name = llvm_block_match2.group(1)
            preds = llvm_block_match2.group(2)
        elif line.startswith(' '):
            if cur_block is not None:
                cur_block.add_line(line)
        elif not line[:1].isspace():
            if line.startswith('}') and block_map:
                break
            cur_block = None

        if block_name is not None:
            cur_block = Block(block_name, preds)
            cur_block.add_line(line)
            block_list.append(cur_block)
            block_map[block_name] = cur_block

    # Add empty blocks which we didn't see, but which are referenced.
    new_blocks = {}
    for block in block_list:
        for adj_name in (block.preds + block.get_succs()):
            if adj_name not in block_map:
                new_blocks[adj_name] = Block(adj_name, None)

    block_map = dict(block_map.items() + new_blocks.items())

    # Add missing edges if we didn't see a successor in the terminator
    # but the block is mentioned in the pred list of the successor.

    for block in block_list:
        for pred_name in block.preds:
            pred_block = block_map[pred_name]
            if block.name not in pred_block.get_succs():
                pred_block.get_succs().append(block.name)

    # Write the output dot file.

    file_name = tempfile.gettempdir() + "/viewcfg" + suffix + ".dot"
    with open(file_name, 'w') as out_file:
        out_file.write('digraph "CFG" {\n')
        for block in block_list:
            if block.content is not None:
                out_file.write(
                    "\tNode" + str(block.index) +
                    " [shape=record,label=\"{" + block.content + "}\"];\n")
            else:
                out_file.write(
                    "\tNode" + str(block.index) +
                    " [shape=record,color=gray,fontcolor=gray,label=\"{" +
                    block.name + "}\"];\n")

            for succ_name in block.get_succs():
                succ_block = block_map[succ_name]
                out_file.write(
                    "\tNode" + str(block.index) + " -> Node" +
                    str(succ_block.index) + ";\n")

        out_file.write("}\n")

    # Open the dot file.
    subprocess.call(["open", "-a", "Graphviz", file_name])


if __name__ == '__main__':
    main()
